/* code stolen from lib ghosd. */

/* ghosd.c -- OSD with fake transparency, cairo, and pango.
 * Copyright (C) 2006 Evan Martin <martine@danga.com>
 */


#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <cairo/cairo-xlib-xrender.h>
#include <pango/pangocairo.h>
#include <X11/Xatom.h>

#include "ghosd.h"
#include "ghosd_internal.h"

static Pixmap
take_snapshot(Ghosd *ghosd) {
  Pixmap pixmap;
  GC gc;

  /* create a pixmap to hold the screenshot. */
  pixmap = XCreatePixmap(ghosd->dpy, ghosd->win,
                         ghosd->width, ghosd->height,
                         DefaultDepth(ghosd->dpy, DefaultScreen(ghosd->dpy)));

  /* then copy the screen into the pixmap. */
  gc = XCreateGC(ghosd->dpy, pixmap, 0, NULL);
  XSetSubwindowMode(ghosd->dpy, gc, IncludeInferiors);
  XCopyArea(ghosd->dpy, DefaultRootWindow(ghosd->dpy), pixmap, gc,
            ghosd->x, ghosd->y, ghosd->width, ghosd->height,
            0, 0);
  XSetSubwindowMode(ghosd->dpy, gc, ClipByChildren);
  XFreeGC(ghosd->dpy, gc);

  return pixmap;
}

void
ghosd_render(Ghosd *ghosd) {
  Pixmap pixmap;
  GC gc;

  /* make our own copy of the background pixmap as the initial surface. */
  pixmap = XCreatePixmap(ghosd->dpy, ghosd->win, ghosd->width, ghosd->height,
                         DefaultDepth(ghosd->dpy, DefaultScreen(ghosd->dpy)));

  gc = XCreateGC(ghosd->dpy, pixmap, 0, NULL);
  if (ghosd->transparent) {
    XCopyArea(ghosd->dpy, ghosd->background, pixmap, gc,
              0, 0, ghosd->width, ghosd->height, 0, 0);
  } else {
    XFillRectangle(ghosd->dpy, pixmap, gc,
				   0, 0, ghosd->width, ghosd->height);
  }
  XFreeGC(ghosd->dpy, gc);

  /* render with cairo. */
  if (ghosd->render_func) {
    /* create cairo surface using the pixmap. */
    XRenderPictFormat *xrformat = 
      XRenderFindVisualFormat(ghosd->dpy,
                              DefaultVisual(ghosd->dpy,
                                            DefaultScreen(ghosd->dpy)));
    cairo_surface_t *surf =
      cairo_xlib_surface_create_with_xrender_format(
													ghosd->dpy, pixmap,
													ScreenOfDisplay(ghosd->dpy, DefaultScreen(ghosd->dpy)),
													xrformat,
													ghosd->width, ghosd->height);

    /* draw some stuff. */
    cairo_t *cr = cairo_create(surf);
    ghosd->render_func(ghosd, cr, ghosd->render_data);
    cairo_destroy(cr);
  }

  /* point window at its new backing pixmap. */
  XSetWindowBackgroundPixmap(ghosd->dpy, ghosd->win, pixmap);
  XFreePixmap(ghosd->dpy, pixmap);

  /* and tell the window to redraw with this pixmap. */
  XClearWindow(ghosd->dpy, ghosd->win);
}

static void
set_hints(Display *dpy, Window win) {
  /* we're almost a _NET_WM_WINDOW_TYPE_SPLASH, but we don't want
   * to be centered on the screen.  instead, manually request the
   * behavior we want. */

  /* turn off window decorations.
   * we could pull this in from a motif header, but it's easier to
   * use this snippet i found on a mailing list.  */
  Atom mwm_hints = XInternAtom(dpy, "_MOTIF_WM_HINTS", False);
#define MWM_HINTS_DECORATIONS (1<<1)
  struct {
    long flags, functions, decorations, input_mode;
  } mwm_hints_setting = {
    MWM_HINTS_DECORATIONS, 0, 0, 0
  };
  XChangeProperty(dpy, win,
				  mwm_hints, mwm_hints, 32, PropModeReplace,
				  (unsigned char *)&mwm_hints_setting, 4);

  /* always on top, not in taskbar or pager. */
  Atom win_state = XInternAtom(dpy, "_NET_WM_STATE", False);
  Atom win_state_setting[] = {
    XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False),
    XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False),
    XInternAtom(dpy, "_NET_WM_STATE_SKIP_PAGER", False)
  };
  XChangeProperty(dpy, win, win_state, XA_ATOM, 32,
                  PropModeReplace, (unsigned char*)&win_state_setting, 3);
}

static Window
make_window(Display *dpy) {
  Window win;
  XSetWindowAttributes att;

  /* XXX I don't understand X well enough to know if these are the correct
   * settings. */
  att.backing_store = WhenMapped;
  att.background_pixel = None;
  att.border_pixel = 0;
  att.background_pixmap = None;
  att.save_under = True;
  att.event_mask = ExposureMask | StructureNotifyMask;
  att.override_redirect = True;

  win = XCreateWindow(dpy, DefaultRootWindow(dpy),
                      -1, -1, 1, 1, 0,
                      CopyFromParent, InputOutput, CopyFromParent,
                      CWBackingStore | CWBackPixel | CWBackPixmap |
                      CWEventMask | CWSaveUnder | CWOverrideRedirect,
                      &att);

  set_hints(dpy, win);

  /* XXX: XSetClassHint? */

  return win;
}

void
ghosd_show(Ghosd *ghosd) {
  XMapWindow(ghosd->dpy, ghosd->win);
}

void
ghosd_set_transparent(Ghosd *ghosd, int transparent) {
  ghosd->transparent = (transparent != 0);
}

void
ghosd_set_render(Ghosd *ghosd, GhosdRenderFunc render_func, void *render_data) {
  ghosd->render_func = render_func;
  ghosd->render_data = render_data;
}

void
ghosd_set_position(Ghosd *ghosd, int x, int y, int width, int height) {
  const int dpy_width  = DisplayWidth(ghosd->dpy,  DefaultScreen(ghosd->dpy));
  const int dpy_height = DisplayHeight(ghosd->dpy, DefaultScreen(ghosd->dpy));

  if (x == GHOSD_COORD_CENTER) {
    x = (dpy_width - width) / 2;
  } else if (x < 0) {
    x = dpy_width - width + x;
  }

  if (y == GHOSD_COORD_CENTER) {
    y = (dpy_height - height) / 2;
  } else if (y < 0) {
    y = dpy_height - height + y;
  }

  ghosd->x      = x;
  ghosd->y      = y;
  ghosd->width  = width;
  ghosd->height = height;

  if (ghosd->transparent)
    ghosd->background = take_snapshot(ghosd);

  XMoveResizeWindow(ghosd->dpy, ghosd->win,
                    ghosd->x, ghosd->y, ghosd->width, ghosd->height);
}

#if 0
static int
x_error_handler(Display *dpy, XErrorEvent* evt) {
  /* segfault so we can get a backtrace. */
  char *x = NULL;
  *x = 0;
  return 0;
}
#endif

Ghosd *
ghosd_new(void) {
  Ghosd *ghosd;
  Display *dpy;
  Window win;

  dpy = XOpenDisplay(NULL);
  if (dpy == NULL) {
    fprintf(stderr, "Couldn't open display: (XXX FIXME)\n");
    return NULL;
  }

  win = make_window(dpy);
  
  ghosd = calloc(1, sizeof(Ghosd));
  ghosd->dpy = dpy;
  ghosd->win = win;
  ghosd->transparent = 1;

  return ghosd;
}

int
ghosd_get_socket(Ghosd *ghosd) {
  return ConnectionNumber(ghosd->dpy);
}


static void
ghosd_main_iteration(Ghosd *ghosd) {
  XEvent ev, pev;
  XNextEvent(ghosd->dpy, &ev);
  
  /* smash multiple configure/exposes into one. */
  if (ev.type == ConfigureNotify) {
    while (XPending(ghosd->dpy)) {
      XPeekEvent(ghosd->dpy, &pev);
      if (pev.type != ConfigureNotify && pev.type != Expose)
        break;
      XNextEvent(ghosd->dpy, &ev);
    }
  }

  switch (ev.type) {
  case Expose:
    break;
  case ConfigureNotify:
    if (ghosd->width > 0) {
      /* XXX if the window manager disagrees with our positioning here,
       * we loop. */
      if (ghosd->x != ev.xconfigure.x ||
          ghosd->y != ev.xconfigure.y) {
        /*width = ev.xconfigure.width; 
		  height = ev.xconfigure.height;*/
        XMoveResizeWindow(ghosd->dpy, ghosd->win,
                          ghosd->x, ghosd->y, ghosd->width, ghosd->height);
      }
    }
    break;
  }
}

void
ghosd_main_iterations(Ghosd *ghosd) {
  while (XPending(ghosd->dpy))
    ghosd_main_iteration(ghosd);
}


void
ghosd_main_until(Ghosd *ghosd, struct timeval *until) {
  struct timeval tv_now;

  ghosd_main_iterations(ghosd);

  for (;;) {
    gettimeofday(&tv_now, NULL);
    int dt = (until->tv_sec  - tv_now.tv_sec )*1000 +
	  (until->tv_usec - tv_now.tv_usec)/1000;
    if (dt <= 0) break;

    struct pollfd pollfd = { ghosd_get_socket(ghosd), POLLIN, 0 };
    int ret = poll(&pollfd, 1, dt);
    if (ret < 0) {
      perror("poll");
      exit(1);
    } else if (ret > 0) {
      ghosd_main_iterations(ghosd);
    } else {
      /* timer expired. */
      break;
    }
  }
}

void ghosd_free(Ghosd *ghosd)
{
  if(! ghosd)
	return;

  XDestroyWindow(ghosd->dpy, ghosd->win);
  XCloseDisplay(ghosd->dpy);
  g_free(ghosd);
}

typedef struct {
  GhosdRenderFunc render_func;
  void *render_data;
  cairo_surface_t* surface;
  float alpha;
} GhosdFlashData;

void
ghosd_text_set_position(Ghosd *ghosd, int x, int y, PangoLayout *layout) {
  PangoRectangle ink_rect;
  pango_layout_get_pixel_extents(layout, &ink_rect, NULL);
  /*printf("rect %d %d %d %d\n",
         ink_rect.x, ink_rect.y,
         ink_rect.width, ink_rect.height);*/

  const int width = ink_rect.x + ink_rect.width+5;
  const int height = ink_rect.y + ink_rect.height+5;

  ghosd_set_position(ghosd, x, y, width, height);
}

static void
ghosd_flash_render(Ghosd *ghosd, cairo_t *cr, void* data) {
  GhosdFlashData *flash = data;

  /* the first time we render, let the client render into their own surface. */
  if (flash->surface == NULL) {
    cairo_t *rendered_cr;
    flash->surface = cairo_surface_create_similar(cairo_get_target(cr),
                                                  CAIRO_CONTENT_COLOR_ALPHA,
                                                  ghosd->width, ghosd->height);
    rendered_cr = cairo_create(flash->surface);
    flash->render_func(ghosd, rendered_cr, flash->render_data);
    cairo_destroy(rendered_cr);
  }

  /* now that we have a rendered surface, all we normally do is copy that to
   * the screen. */
  cairo_set_source_surface(cr, flash->surface, 0, 0);
  cairo_paint_with_alpha(cr, flash->alpha);
}

void
ghosd_flash(Ghosd *ghosd, int fade_ms, int total_display_ms) {
  GhosdFlashData flash = {0};
  flash.render_func = ghosd->render_func;
  flash.render_data = ghosd->render_data;
  ghosd->render_func = ghosd_flash_render;
  ghosd->render_data = &flash;

  ghosd_render(ghosd);
  ghosd_show(ghosd);

  const int STEP_MS = 50;
  const float dalpha = 1.0 / (fade_ms / (float)STEP_MS);
  struct timeval tv_nextupdate;

  /* fade in. */
  for (flash.alpha = 0; flash.alpha < 1.0; flash.alpha += dalpha) {
    if (flash.alpha > 1.0) flash.alpha = 1.0;
    ghosd_render(ghosd);

    gettimeofday(&tv_nextupdate, NULL);
    tv_nextupdate.tv_usec += STEP_MS*1000;
    ghosd_main_until(ghosd, &tv_nextupdate);
  }

  /* full display. */
  flash.alpha = 1.0;
  ghosd_render(ghosd);

  gettimeofday(&tv_nextupdate, NULL);
  tv_nextupdate.tv_usec += (total_display_ms - (2*fade_ms))*1000;
  ghosd_main_until(ghosd, &tv_nextupdate);

  /* fade out. */
  for (flash.alpha = 1.0; flash.alpha > 0.0; flash.alpha -= dalpha) {
    ghosd_render(ghosd);

    gettimeofday(&tv_nextupdate, NULL);
    tv_nextupdate.tv_usec += STEP_MS*1000;
    ghosd_main_until(ghosd, &tv_nextupdate);
  }

  flash.alpha = 0;
  ghosd_render(ghosd);
  ghosd_main_iterations(ghosd);
}


/***************** testing part ********************/
#ifdef TEST_GHOSD

static void
render(Ghosd *ghosd, cairo_t *cr, void* data) {
  PangoLayout *layout = data;

  /* drop shadow! */
  cairo_set_source_rgba(cr, 0, 0, 0, 0.5);
  cairo_move_to(cr, 2, 2);
  pango_cairo_show_layout(cr, layout);

  /*   and the actual text. */
  cairo_set_source_rgba(cr, 1, 1, 1, 0.8);
  cairo_move_to(cr, 0, 0);
  pango_cairo_show_layout(cr, layout);
}

int main(int argc, char* argv[]) {
  Ghosd *ghosd;
  struct timeval tv;
  g_type_init();

  PangoContext *context;
  PangoLayout *layout;
  context = pango_cairo_font_map_create_context(
              PANGO_CAIRO_FONT_MAP(pango_cairo_font_map_get_default()));
  layout = pango_layout_new(context);
  pango_layout_set_markup(layout,
                          "<span font_desc='Trebuchet 30'>"
                          "some sample text using <b>ghosd</b>"
                          "</span>", -1);

  ghosd = ghosd_new();
  ghosd_text_set_position(ghosd, -50, -50, layout);
  ghosd_set_render(ghosd, render, layout);

/*   ghosd_render(ghosd); */
/*   ghosd_show(ghosd); */
/*   gettimeofday(&tv, NULL); */
/*   tv.tv_sec += 5; */
/*   ghosd_main_until(ghosd, &tv); */
  ghosd_flash(ghosd, 300, 1500);

  return 0;
}

#endif
